﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace SpectrumAnalyser.ServiceClientProxy
{
    /// <summary>
    /// Generate proxy type for service on client.
    /// </summary>
    public class ServiceClientGenerator
    {
        private static readonly AssemblyBuilder _assemblyBuilder;

        private readonly Type _serviceContractType;
        protected Type _baseServiceClientType;
        protected TypeBuilder _serviceClientTypeBuilder;

        /// <summary>
        /// Initializes <see cref="ServiceClientGenerator"/> type.
        /// </summary>
        static ServiceClientGenerator()
        {
            var dynamicAssemblyName = new AssemblyName(Guid.NewGuid().ToString());
            _assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(dynamicAssemblyName, AssemblyBuilderAccess.RunAndCollect);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ServiceClientGenerator"/> class.
        /// </summary>
        /// <param name="serviceContractType">Service contract type.</param>
        public ServiceClientGenerator(Type serviceContractType)
        {
            this._serviceContractType = serviceContractType;
        }

        /// <summary>
        /// Creates service client type at runtime.
        /// </summary>
        /// <returns>Created service client type.</returns>
        public Type CreateProxyType()
        {
            this.CreateTypeBuilder();
            this.AddConstructor();

            foreach (MethodInfo method in this.GetMethodsToImplement())
            {
                this.ImplementInterfaceMethod(method);
            }

            Type type = this._serviceClientTypeBuilder.CreateType();
            //_assemblyBuilder.Save(String.Format("{0}.dll", ASSEMBLY_NAME));
            return type;
        }

        private void CreateTypeBuilder()
        {
            const TypeAttributes newAttributes = TypeAttributes.Public | TypeAttributes.Class;

            this.CreateBaseType();

            ModuleBuilder dynamicModuleBuilder = _assemblyBuilder.DefineDynamicModule(this.GetModuleName(), this.GetModuleFileName(), true);
            this._serviceClientTypeBuilder = dynamicModuleBuilder.DefineType(this.GetTypeName(), newAttributes,
                                                                             this._baseServiceClientType,
                                                                             new[] { this._serviceContractType });
        }

        private void CreateBaseType()
        {
            this._baseServiceClientType = this.GetBaseTypeGenericDefenition().MakeGenericType(this._serviceContractType);
        }

        protected virtual Type GetBaseTypeGenericDefenition()
        {
            return typeof(DebugClientBase<>);
        }

        private IEnumerable<MethodInfo> GetMethodsToImplement()
        {
            var interfaceMethods = this._serviceContractType.GetMethods().ToList();
            return interfaceMethods;
        }

        protected virtual void AddConstructor()
        {
            var constructorArgs = new[] { typeof(Binding), typeof(EndpointAddress) };
            ConstructorBuilder constructorBuilder = this._serviceClientTypeBuilder.DefineConstructor(MethodAttributes.Public,
                                                                                                     CallingConventions.Standard,
                                                                                                     constructorArgs);
            constructorBuilder.DefineParameter(1, ParameterAttributes.None, "binding");
            constructorBuilder.DefineParameter(2, ParameterAttributes.None, "endpoint");
            // Generate IL for constructor that calls base class constructor
            ILGenerator contstructorIL = constructorBuilder.GetILGenerator();
            contstructorIL.Emit(OpCodes.Ldarg_0);
            contstructorIL.Emit(OpCodes.Ldarg_1);
            contstructorIL.Emit(OpCodes.Ldarg_2);
            contstructorIL.Emit(OpCodes.Call,
                                this._baseServiceClientType.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null,
                                                                           constructorArgs, null));
            contstructorIL.Emit(OpCodes.Ret);
        }

        private void ImplementInterfaceMethod(MethodInfo method)
        {
            Type[] parametersTypes = method.GetParameters().Select(pi => pi.ParameterType).ToArray();

            MethodBuilder methodBuilder = this._serviceClientTypeBuilder.DefineMethod(method.Name,
                                                                                      MethodAttributes.Public | MethodAttributes.Virtual |
                                                                                      MethodAttributes.Final | MethodAttributes.NewSlot,
                                                                                      method.ReturnType, parametersTypes);

            if (method.IsGenericMethod)
            {
                Type[] genericParameters = method.GetGenericArguments();
                methodBuilder.DefineGenericParameters(genericParameters.Select(p => p.Name).ToArray());
                methodBuilder.MakeGenericMethod(genericParameters);
            }
            this._serviceClientTypeBuilder.DefineMethodOverride(methodBuilder, method);

            ILGenerator methodIL = methodBuilder.GetILGenerator();

            // this.Channel.SomeMethod(arg1, arg2, ..., argN);
            methodIL.Emit(OpCodes.Ldarg_0);
            methodIL.Emit(OpCodes.Callvirt, this.GetGetChannelMethod());
            for (int arg = 1; arg <= method.GetParameters().Length; arg++)
            {
                methodIL.Emit(OpCodes.Ldarg, arg);
            }
            methodIL.EmitCall(OpCodes.Callvirt, method, null);

            methodIL.Emit(OpCodes.Ret);
        }

        private string GetModuleFileName()
        {
            return String.Format("{0}.mod", this.GetModuleName());
        }

        private string GetModuleName()
        {
            return this.GetTypeName();
        }

        private string GetTypeName()
        {
            return String.Format("AutoGeneratedServiceClient_{0}", this._serviceContractType.Name);
        }

        private MethodInfo GetGetChannelMethod()
        {
            PropertyInfo channelPropertyInfo = this._baseServiceClientType
                .GetProperty("Channel", BindingFlags.NonPublic | BindingFlags.Public |
                                        BindingFlags.Instance | BindingFlags.FlattenHierarchy);
            return channelPropertyInfo.GetGetMethod() ?? channelPropertyInfo.GetGetMethod(true);
        }
    }
}
